package com.ai.wxy.spring.web.argument;

import com.ai.spring.common.annotation.RequestParam;
import com.ai.spring.common.enums.ExceptionEnum;
import com.ai.spring.common.enums.ParamScope;
import com.ai.spring.common.enums.ResponseCodeEnum;
import com.ai.spring.common.exception.WebException;
import com.ai.spring.common.util.IOUtils;
import com.ai.spring.common.util.ReflectUtil;
import com.ai.spring.common.util.ResponseResult;
import com.ai.spring.common.util.ResponseResultUtil;
import com.ai.wxy.spring.service.api.ServiceCaller;
import com.ai.wxy.spring.service.api.dto.*;
import com.ai.wxy.spring.web.util.Constans;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.HandlerMapping;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;

/**
 * 自定义参数解析器
 *
 * @author 石头
 * @Date 2019/11/18
 * @Version 1.0
 **/
@Slf4j
public class RequestParamArgumentResolver implements HandlerMethodArgumentResolver {
    private ServiceCaller serviceCaller;
    public RequestParamArgumentResolver(ServiceCaller serviceCaller){
        this.serviceCaller = serviceCaller;
    }
    /**
     * 解析器是否支持当前参数
     * @param methodParameter
     * @return
     */
    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        return methodParameter.hasParameterAnnotation(RequestParam.class);
    }

    /**
     * 将request中的请求参数解析到当前Controller参数上
     * @param parameter        需要被解析的Controller参数，此参数必须首先传给{@link #supportsParameter}并返回true
     * @param mavContainer     当前request的ModelAndViewContainer
     * @param nativeWebRequest 当前request
     * @param binderFactory    生成{@link WebDataBinder}实例的工厂
     * @author 石头
     * @date 2019/11/18
     * @return java.lang.Object      解析后的Controller参数
     **/
    @Override
    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest nativeWebRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
        String requestId = nativeWebRequest.getHeader("requestId");
        ServiceConf serviceConf = getServiceConf(requestId);
        if (serviceConf == null){
            throw new WebException(ResponseCodeEnum.SERVICE_ID_ERROR.getCode(),ResponseCodeEnum.SERVICE_ID_ERROR.getName());
        }
        RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
        ParamScope paramScope = requestParam.scope();

        try{
            Class<?> targetType  = getParamType(serviceConf);
            Object arg = getServiceParam(serviceConf,paramScope,nativeWebRequest);
            if (ParamScope.PATH == paramScope){
                WebDataBinder binder = binderFactory.createBinder(nativeWebRequest, null, serviceConf.getParamName());
                arg = binder.convertIfNecessary(arg, targetType, parameter);
            }else if(ParamScope.REQ_PARAM == paramScope) {
                String mapStr = JSON.toJSONString(arg);
                arg = JSON.parseObject(mapStr,targetType);
            }else if(ParamScope.REQ_BODY == paramScope){
                arg = JSON.parseObject(arg.toString(),targetType);
            }
            // 封装参数
            return getServiceContext(arg,serviceConf,targetType);
        }catch (Throwable e){
            log.warn(e.getMessage(),e);
            throw new WebException(ExceptionEnum.ERROR_PARAM.getCode(),ResponseCodeEnum.ERROR_500.getName());
        }
    }
    private ServiceContext getServiceContext(Object arg,ServiceConf serviceConf,Class<?> targetType){
        Class[] paramCls = null;
        ServiceParam param = null;
        if (ServiceParam.class.isAssignableFrom(targetType)){
            paramCls = new Class[]{targetType};
            param = (ServiceParam)arg;
        }else {
            paramCls = new Class[]{DefaultServiceParam.class};
            param = new DefaultServiceParam(arg);
        }

        ServiceContext context = new ServiceContext();
        context.setRequestId(serviceConf.getRequestId());
        context.setServiceId(serviceConf.getServiceId());
        context.setMethodName(serviceConf.getServiceMethod());
        context.setParamCls(paramCls);
        context.setParamVals(new ServiceParam[]{param});

        return context;
    }
    private Object getServiceParam(ServiceConf serviceConf,ParamScope paramScope,NativeWebRequest nativeWebRequest){

        Object paramValue = null;
        try{
            if (ParamScope.PATH == paramScope){
                paramValue = getPathParamVal(nativeWebRequest,serviceConf.getParamName());
            }else if(ParamScope.REQ_PARAM == paramScope){
                paramValue = getParamVal(nativeWebRequest);
            }else if(ParamScope.REQ_BODY == paramScope){
                paramValue = getBodyVal(nativeWebRequest);
            }else {
                log.warn("[{}]"+ ExceptionEnum.ERROR_REQ_SCOPE.getName(),ExceptionEnum.ERROR_REQ_SCOPE.getCode(),paramScope);
                throw new WebException(ExceptionEnum.ERROR_REQ_SCOPE.getCode(),ResponseCodeEnum.ERROR_500.getName());
            }
            return paramValue;
        }catch (Throwable e){
            log.warn(e.getMessage(),e);
            throw new WebException(ExceptionEnum.ERROR_PARAM.getCode(),ResponseCodeEnum.ERROR_500.getName());
        }
    }

    private String getBodyVal(NativeWebRequest nativeWebRequest)throws IOException {
        HttpServletRequest servletRequest = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
        ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
        InputStream inputStream = null;
        try{
            inputStream = inputMessage.getBody();
            String json = IOUtils.read(new InputStreamReader(inputStream));
            return json;
        }finally {
            IOUtils.closeStream(inputStream);
        }
    }
    private Map<String,String> getParamVal(NativeWebRequest nativeWebRequest){
        Map<String, String> map = new HashMap<>();
        Map<String, String[]> params = nativeWebRequest.getParameterMap();
        params.entrySet().stream().forEach(entry -> {
            map.put(entry.getKey(),entry.getValue()[0]);
        });
        return map;
    }
    private Object getPathParamVal(NativeWebRequest nativeWebRequest,String paramName){
        Map<String, String> uriTemplateVars = (Map<String, String>) nativeWebRequest.getAttribute(
                HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
        Object paramValue = (uriTemplateVars != null ? uriTemplateVars.get(paramName) : null);

        return paramValue;
    }
    private Class<?> getParamType(ServiceConf serviceConf){
        Class<?> paramType = ReflectUtil.forName(serviceConf.getParamType());
        return paramType;
    }
    private ServiceConf getServiceConf(String requestId){
        Class[] paramCls = new Class[]{DefaultServiceParam.class};
        DefaultServiceParam<String> param = new DefaultServiceParam(requestId);

        ServiceContext context = new ServiceContext();
        context.setRequestId(requestId);
        context.setServiceId(Constans.SVC_CONF_ID);
        context.setMethodName(Constans.SVC_CONF_METHOD);
        context.setParamCls(paramCls);
        context.setParamVals(new ServiceParam[]{param});

        ResponseResult<ServiceResult> result = serviceCaller.excute(context);
        if (ResponseResultUtil.isSuccess(result)){
            return (ServiceConf)result.getData();
        }
        log.error("获取服务接口[{}]配置信息失败:{}",requestId,result.getMsg());
        return null;
    }
}
